﻿// -----------------------------------------------------------------------
// <copyright file="Render.cs" company="Abyssalsoft">
// TODO: Update copyright text.
// </copyright>
// -----------------------------------------------------------------------

namespace Core.Player.Video
{
    using System;
    using System.Collections.Generic;
    using System.Runtime.InteropServices;
    using System.Text;
    using System.Windows.Forms;
    using Core.Player.Video.DShow;

    /// <summary>
    /// TODO: Update summary.
    /// </summary>
    internal class Render
    {
        #region Declarations

        /// <summary>Constant - not used yet</summary>
        private const int WMAPP = 0x8000;

        /// <summary>Constant - not used yet</summary>
        private const int WMGRAPHNOTIFY = WMAPP + 1;

        /// <summary>Object video window constant, used for WindowStyle</summary>
        private const int WSCHILD = 0x40000000;

        /// <summary>Object video window constant, used for WindowStyle</summary>
        private const int WSCLIPCHILDREN = 0x2000000;

        /// <summary> Lock synchronization object. </summary>
        private static readonly object Padlock = new object();

        /// <summary> Stores the current instance of this object </summary>
        private static Render instance = null;

        /// <summary> The picture box object where we put the video </summary>
        private PictureBox videoFrame;

        ///// <summary>Constant - not used yet</summary>
        ////private const int ECCOMPLETE = 0x01;

        /// <summary> A timer used to update some variables </summary>
        private System.Timers.Timer videoTimer;

        [DllImport("kernel32.dll", CharSet = CharSet.Auto, SetLastError = true)]
        static extern EXECUTION_STATE SetThreadExecutionState(EXECUTION_STATE flags);

        public enum EXECUTION_STATE : uint
        {
            ES_SYSTEM_REQUIRED = 0x00000001,
            ES_DISPLAY_REQUIRED = 0x00000002,
            ES_CONTINUOUS = 0x80000000
        }

        #endregion

        #region DirectShow interfaces

        /// <summary>Quartz interface - FilterGraph class</summary>
        private IGraphBuilder objFilterGraph = null;

        /// <summary>Quartz interface - BasicAudio class</summary>
        private IBasicAudio objBasicAudio = null;

        /// <summary>Quartz interface - BasicVideo class</summary>
        private IBasicVideo2 objBasicVideo = null;

        /// <summary>Quartz interface - Video window class</summary>
        private IVideoWindow objVideoWindow = null;

        /// <summary>Quartz interface - Media event class</summary>
        private IMediaEvent objMediaEvent = null;

        /// <summary>Quartz interface - Media event ex class</summary>
        private IMediaEventEx objMediaEventEx = null;

        /// <summary>Quartz interface - Media position class</summary>
        private IMediaPosition objMediaPosition = null;

        /// <summary>Quartz interface - Media control class</summary>
        private IMediaControl objMediaControl = null;

        #endregion

        /// <summary> Prevents a default instance of the Render class from being created. </summary>
        private Render()
        {
            // Timer updater
            this.videoTimer = new System.Timers.Timer();
            this.videoTimer.Elapsed += new System.Timers.ElapsedEventHandler(this.TimerTick);
            this.videoTimer.Interval = 1000;
        }

        /// <summary> Gets the current instance of this class (Singleton - Thread safety) </summary>
        public static Render Instance
        {
            get
            {
                if (instance == null)
                {
                    lock (Padlock)
                    {
                        if (instance == null)
                        {
                            instance = new Render();
                        }
                    }
                }

                return instance;
            }
        }

        #region Events

        /// <summary>
        /// Open file
        /// </summary>
        internal void Open()
        {
            if (Helper.CheckFileType(MediaInfo.FilePath) != Helper.FileTypeEnum.LocalVideo)
            {
                return;
            }

            // Clear objects
            ////this.Dispose(sender, e);

            // Initialize filter graph
            ////this.objFilterGraph = new IFilterGraph();
            ////this.objFilterGraph = new IGraphBuilder();
            object comobj = null;
            Type comtype = null;

            comtype = Type.GetTypeFromCLSID(Clsid.FilterGraph);
            comobj = Activator.CreateInstance(comtype);
            this.objFilterGraph = (IGraphBuilder)comobj;
            comobj = null;

            // Build the graph automatically for this file. In this way, filter
            // graph manager connects appropiate filters for specified file.
            // This is called "Intelligent connect". :P Actualy, I'm to bored now
            // to build manualy the filter graph.
            this.objFilterGraph.RenderFile(MediaInfo.FilePath, string.Empty);

            // Set objBasicAudio
            this.objBasicAudio = this.objFilterGraph as IBasicAudio;

            // Set objBasicVideo
            this.objBasicVideo = this.objFilterGraph as IBasicVideo2;

            // Get video original size
            int sourceHeight;
            int sourceWidth;
            this.objBasicVideo.get_SourceWidth(out sourceWidth);
            this.objBasicVideo.get_SourceHeight(out sourceHeight);
            MediaInfo.VideoSourceHeight = sourceHeight;
            MediaInfo.VideoSourceWidth = sourceWidth;

            // Get some info
            int bitRate;
            double averageTimePerFrame;
            this.objBasicVideo.get_BitRate(out bitRate);
            this.objBasicVideo.get_AvgTimePerFrame(out averageTimePerFrame);
            MediaInfo.VideoBitRate = bitRate;
            MediaInfo.AverageTimePerFrame = averageTimePerFrame * 1000;

            // Info Header
            //MediaInfo.Compression = this.objInfoHeader.BmiHeader.Compression;
            //MediaInfo.ClrUsed = this.objInfoHeader.BmiHeader.ClrUsed;
            //MediaInfo.VideoPlanes = this.objInfoHeader.BmiHeader.Planes;
            //MediaInfo.HeaderSize = this.objInfoHeader.BmiHeader.Size;

            // Set Media Event
            this.objMediaEvent = this.objFilterGraph as IMediaEvent;
            this.objMediaEventEx = this.objFilterGraph as IMediaEventEx;

            // Set Media Position
            this.objMediaPosition = this.objFilterGraph as IMediaPosition;

            // Set Media Control
            this.objMediaControl = this.objFilterGraph as IMediaControl;

            // Update volume
            this.SetVolume();
        }

        /// <summary>
        /// Start playback
        /// </summary>
        internal void Play()
        {
            try
            {                
                // Set objVideoWindow
                this.objVideoWindow = this.objFilterGraph as IVideoWindow;
                this.objVideoWindow.put_Owner((IntPtr)this.videoFrame.Handle);
                this.objVideoWindow.put_WindowStyle(WindowStyle.Child);
                this.objVideoWindow.SetWindowForeground(OABool.True);
                this.objVideoWindow.SetWindowPosition(
                    this.videoFrame.ClientRectangle.Left,
                    this.videoFrame.ClientRectangle.Top,
                    this.videoFrame.ClientRectangle.Width,
                    this.videoFrame.ClientRectangle.Height);
                this.objVideoWindow.put_Caption("Metal Player Video Window");

                this.videoTimer.Start();
            }
            catch (Exception)
            {
                this.objVideoWindow = null;
            }

            this.objMediaEventEx.SetNotifyWindow((IntPtr)this.videoFrame.Handle, WMGRAPHNOTIFY, IntPtr.Zero);

            this.objMediaControl.Run();
            
            ////double duration;
            ////int bRate;
            ////objMediaPosition.get_Duration(out duration);
            ////this.objBasicVideo.get_BitRate(out bRate);
            ////MetalFwk.MediaInfo.TotalTimeInBytes = Convert.ToInt32(duration);
            ////MetalFwk.MediaInfo.VideoBitRate = bRate;
        }

        /// <summary>
        /// Pause the current playback
        /// </summary>
        internal void Pause()
        {
            if (Helper.CheckFileType(MediaInfo.FilePath) != Helper.FileTypeEnum.LocalVideo)
            {
                return;
            }

            this.objMediaControl.Pause();
        }

        /// <summary>
        /// Stop the current playback
        /// </summary>
        /// <param name="sender">Object sender</param>
        /// <param name="e">Event args</param>
        internal void Stop(object sender, EventArgs e)
        {
            try
            {
                if (Helper.CheckFileType(MediaInfo.FilePath) != Helper.FileTypeEnum.LocalVideo)
                {
                    return;
                }

                // Save Video window position
                this.objMediaPosition.put_CurrentPosition(0);
                this.objMediaControl.Stop();
                this.ReleaseObjects(sender, e);

                this.videoTimer.Stop();
            }
            catch
            {
            }
        }
        
        /// <summary>
        /// Resume playback
        /// </summary>
        internal void Resume()
        {
            if (Helper.CheckFileType(MediaInfo.FilePath) != Helper.FileTypeEnum.LocalVideo)
            {
                return;
            }

            this.objMediaControl.Run();
        }

        /// <summary>
        /// Seek media
        /// </summary>
        /// <param name="value">New value</param>
        internal void Seek(int value)
        {
            try
            {
                if (Helper.CheckFileType(MediaInfo.FilePath) != Helper.FileTypeEnum.LocalVideo)
                {
                    return;
                }

                double pllTime;
                this.objMediaPosition.put_CurrentPosition(value);
                this.objMediaPosition.get_CurrentPosition(out pllTime);
            }
            catch
            {
            }
        }

        /// <summary>
        /// Resize video frame
        /// </summary>
        /// <param name="sender">Object sender</param>
        /// <param name="e">Event args</param>
        internal void OnVideoResize(object sender, EventArgs e)
        {
            if (MediaInfo.CurrentFile != Helper.FileTypeEnum.LocalVideo)
            {
                return;
            }

            if (this.objVideoWindow != null)
            {
                this.objVideoWindow.SetWindowPosition(
                    this.videoFrame.ClientRectangle.Left,
                    this.videoFrame.ClientRectangle.Top,
                    this.videoFrame.ClientRectangle.Width,
                    this.videoFrame.ClientRectangle.Height);
            }
        }

        /// <summary>
        /// Release used objects
        /// </summary>
        /// <param name="sender">Object sender</param>
        /// <param name="e">Event args</param>
        internal void ReleaseObjects(object sender, EventArgs e)
        {
            if (Helper.CheckFileType(MediaInfo.FilePath) != Helper.FileTypeEnum.LocalVideo)
            {
                return;
            }

            if (this.objMediaControl != null)
            {
                this.objMediaControl.Stop();
            }

            if (this.objMediaEventEx != null)
            {
                this.objMediaEventEx.SetNotifyWindow(IntPtr.Zero, 0, IntPtr.Zero);
            }

            if (this.objVideoWindow != null)
            {
                this.objVideoWindow.put_Visible(0);
                this.objVideoWindow.put_Owner(IntPtr.Zero);
            }

            ////Marshal.ReleaseComObject(this.objMediaControl);
            this.objMediaControl = null;

            ////Marshal.ReleaseComObject(this.objMediaPosition);
            this.objMediaPosition = null;

            ////Marshal.ReleaseComObject(this.objMediaEventEx);
            this.objMediaEventEx = null;

            ////Marshal.ReleaseComObject(this.objMediaEvent);
            this.objMediaEvent = null;

            ////Marshal.ReleaseComObject(this.objVideoWindow);
            this.objVideoWindow = null;

            ////Marshal.ReleaseComObject(this.objBasicAudio);
            this.objBasicAudio = null;

            ////Marshal.ReleaseComObject(this.objBasicVideo);
            this.objBasicVideo = null;

            ////Marshal.ReleaseComObject(this.objVideoWindow);
            this.objVideoWindow = null;

            ////Marshal.ReleaseComObject(this.objFilterGraph);
            this.objFilterGraph = null;
        }

        #endregion

        #region Methods

        /// <summary>
        /// Set the PictureBox object, where to render the video
        /// </summary>
        /// <param name="frame">The picture box where to render the video</param>
        internal void SetOutputFrame(ref PictureBox frame)
        {
            this.videoFrame = frame;

            Events.Instance.Dispose += new EventHandler(this.ReleaseObjects);
            frame.Resize += new EventHandler(this.OnVideoResize);
        }

        /// <summary>
        /// Set the volume
        /// </summary>
        internal void SetVolume()
        {
            // TODO: Video volume has some problems with the values, are not really correct!
            // Scrollbar has 0 to 100 values, but objBasicAudio need -5000 to 0 values.
            // So... here i've made some triks. 0 must be -5000 and 100 to be 0.
            int volume = Status.Volume;
            int tempValue;
            volume = 100 - volume;
            ////tempValue = volume - volume - volume; // Invert sign: + to -
            tempValue = volume * -1; // Invert sign: + to -
            volume = tempValue * 50;

            // If scrollbar is not on 0
            if (volume != -5000)
            {
                this.objBasicAudio.put_Volume(volume / 2);
            }
            else
            {
                this.objBasicAudio.put_Volume(-10000); // Mute
            }
        }

        #endregion

        #region Properties

        /// <summary>
        /// Get total time
        /// </summary>
        /// <returns>Returns the total time in seconds</returns>
        internal double GetTotalTimeInSeconds()
        {
            if (this.objMediaPosition == null)
            {
                return 0;
            }

            double totalTime;
            this.objMediaPosition.get_Duration(out totalTime);

            ////int s = Convert.ToInt32(d);
            ////int h = s / 3600;
            ////int m = (s - (h * 3600)) / 60;
            ////s = s - (h * 3600 + m * 60);

            return totalTime;
        }

        /// <summary>
        /// Get total time
        /// </summary>
        /// <returns>Returns the total time in bytes</returns>
        internal long GetTotalTimeInBytes()
        {
            double totalTime;
            this.objMediaPosition.get_Duration(out totalTime);
            return (long)totalTime;
        }

        /// <summary>
        /// Get current position
        /// </summary>
        /// <returns>Returns the current position in seconds</returns>
        internal double GetCurrentPositionInSeconds()
        {
            if (this.objMediaPosition == null)
            {
                return 0;
            }

            double pos;
            this.objMediaPosition.get_CurrentPosition(out pos);
            ////int s = Convert.ToInt32(pos);
            ////int h = s / 3600;
            ////int m = (s - (h * 3600)) / 60;
            ////s = s - (h * 3600 + m * 60);

            return pos;
        }

        /// <summary>
        /// Get current position
        /// </summary>
        /// <returns>Returns the current position in bytes</returns>
        internal long GetCurrentPositionInBytes()
        {
            double pllTime;
            this.objMediaPosition.get_CurrentPosition(out pllTime);
            return (long)pllTime;
        }

        /// <summary>
        /// Get total time formatted
        /// </summary>
        /// <returns>Returns the total time formatted</returns>
        internal string GetTotalTimeFormatted()
        {
            return string.Empty;
        }

        /// <summary>
        /// Get current position
        /// </summary>
        /// <returns>Returns the current position formatted</returns>
        internal string GetCurrentPositionFormatted()
        {
            return string.Empty;
        }

        #endregion

        /// <summary>
        /// Used to update some variables
        /// </summary>
        /// <param name="sender">Object sender</param>
        /// <param name="e">Event args e</param>
        private void TimerTick(object sender, EventArgs e)
        {
            if (this.objMediaEventEx == null)
            {
                return;
            }

            // Check if the video playback ended (should be improved)
            EventCode lEventCode;
            IntPtr lParam1, lParam2;

            this.objMediaEventEx.GetEvent(out lEventCode, out lParam1, out lParam2, 0);

            this.objMediaEventEx.FreeEventParams(lEventCode, lParam1, lParam2);

            if (lEventCode == EventCode.Complete)
            {
                if (Events.Instance.PlaybackEnded != null)
                {
                    Status.PlaybackStatus = Status.StatusEnum.Stopped;
                    Events.Instance.PlaybackEnded(this, e);
                }
            }

            KeepAlive();
        }

        /// <summary>
        /// Keep the display on (no dim, no stupid screen saver when running my movie!!)
        /// </summary>
        private void KeepAlive()
        {
            SetThreadExecutionState(EXECUTION_STATE.ES_DISPLAY_REQUIRED | EXECUTION_STATE.ES_SYSTEM_REQUIRED | EXECUTION_STATE.ES_CONTINUOUS);
        }
    }
}
